<template>
  <div
    class="text-scroll-horizontal"
    :class="[{ 'is-hover-stop': animateOptions.hoverStop && isAnimate }, customClass]"
  >
    <div
      v-if="isOverflowScroll"
      ref="wrapperRef"
      class="text-scroll-horizontal-wrapper"
      :style="{
        '--duration': `${animateOptions.duration}s`,
        '--delay': `${animateOptions.delay}s`,
        '--direction': `${animateOptions.direction}`,
        '--end-distance': `${animateOptions.endDistance}px`,
        '--space-width': `${animateOptions.spaceWidth}px`,
        '--start-distance': `${animateOptions.startDistance}px`,
      }"
      :class="{ 'is-animate': isAnimate }"
    >
      <div ref="textRef" class="text-inline">
        {{ localValue }}
      </div>
      <div v-if="isAnimate" class="text-inline copy-text">{{ localValue }}</div>
    </div>
    <div class="text-inline text-over" v-else>{{ localValue }}</div>
  </div>
</template>

<script>
import { nextTick } from "vue";
import { observerDom } from './utils/common'

export default {
  name: "TextScrollHorizontal",
  props: {
    /**
     * 文本内容
     */
    value: {
      type: String,
      default: "",
    },
    /**
     * 是否溢出滚动（是否开启滚动动画）
     */
    isOverflowScroll: {
      type: Boolean,
      default: true,
    },
    /**
     * # 滚动动画配置
     * @param duration `number` 动画持续时间（s），滚动一次当前容器宽度所需时长，默认值为`5s`
     * @param delay `number` 动画延迟时间（s）。默认值为`0s`
     * @param direction `string` 动画方向，可选值：`left`|`right`，默认值为`left`
     * @param spaceWidth `number|string` 无限滚动首尾空白宽度，数字为像素值，字符串为百分比值（相对于容器宽度），默认值为`50%`
     * @param hoverStop `boolean` 鼠标悬停时是否停止动画，默认值为`true`
     * @param fixSpeed `number` 固定滚动速度，单位为像素/秒。若设置了固定滚动速度（`fixSpeed > 0`），则`duration`不生效。默认值为`0`
     */
    animateConfig: {
      type: Object,
      default: () => ({}),
    },
    /**
     * # 自定义样式名称
     */
    customClass: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      localValue: "",
      textWidth: 0,
      parentWidth: 0,
    };
  },
  computed: {
    isAnimate() {
      return this.isOverflowScroll && this.textWidth > this.parentWidth;
    },
    animateOptions() {
      // 默认动画配置
      const {
        delay = 0,
        direction = "left",
        hoverStop = true,
      } = this.animateConfig;
      // 计算首尾间距
      const spaceDistance = this.getSpaceDistance();
      // 移动距离
      const moveDistance = this.textWidth + spaceDistance;
      // 计算动画起始位置
      const { startDistance, endDistance } =
        this.getAnimateDistance(moveDistance);
      // 计算动画周期
      const moveDuration = this.getAnimateDuration(moveDistance);

      return {
        duration: moveDuration,
        delay,
        direction,
        spaceWidth: spaceDistance,
        hoverStop,
        startDistance,
        endDistance,
      };
    },
  },
  mounted() {
    this.localValue = this.value;
    observerDom(this.$el, ({ width }) => {
      if (this.parentWidth === width) {
        return;
      }
      this.parentWidth = width;
      this.reset();
    });
  },
  methods: {
    /**
     * # 计算内容宽度
     */
    computedTextWidth() {
      const textRef = this.$refs.textRef;
      this.textWidth = textRef.clientWidth;
    },
    /**
     * # 获取首尾像素间距
     */
    getSpaceDistance() {
      const { spaceWidth = "50%" } = this.animateConfig;
      return typeof spaceWidth === "string"
        ? (parseFloat(spaceWidth) / 100) * this.parentWidth
        : spaceWidth;
    },
    /**
     * # 获取动画起始位置
     * @param moveDistance `number` 移动距离
     * @returns 动画起始位置
     */
    getAnimateDistance(moveDistance) {
      const { direction = "left" } = this.animateConfig;
      // 计算动画起始位置
      let startDistance;
      let endDistance;
      // 向右移动
      if (direction === "right") {
        startDistance = -moveDistance;
        endDistance = 0;
      } else {
        startDistance = 0;
        endDistance = -moveDistance;
      }
      return {
        startDistance,
        endDistance,
      };
    },
    /**
     * # 计算动画周期
     * @param moveDistance `number` 移动距离
     * @returns 动画周期
     */
    getAnimateDuration(moveDistance) {
      // 默认动画配置
      const { duration = 5, fixSpeed = 0 } = this.animateConfig;
      let moveDuration;
      if (fixSpeed > 0) {
        moveDuration = moveDistance / fixSpeed;
      } else {
        moveDuration = (this.textWidth / this.parentWidth) * duration;
      }
      return moveDuration;
    },
    /**
     * # 重置动画
     */
    async reset() {
      await nextTick();
      if (!this.$refs.textRef) {
        return;
      }
      this.computedTextWidth();
    },
  },
  watch: {
    value(newValue) {
      this.localValue = newValue;
      this.reset();
    },
  },
};
</script>

<style scoped>
.text-scroll-horizontal {
  width: 100%;
  overflow: hidden;
  position: relative;
}
.text-scroll-horizontal.is-hover-stop:hover
  .text-scroll-horizontal-wrapper.is-animate {
  animation-play-state: paused;
}
.text-inline {
  display: inline-block;
  white-space: nowrap;
}
.text-over {
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.copy-text {
  margin-left: var(--space-width);
}
.text-scroll-horizontal-wrapper {
  display: flex;
  position: relative;
}
.text-scroll-horizontal-wrapper.is-animate {
  animation: text-scroll-horizontal var(--duration) var(--delay) infinite linear;
}

@keyframes text-scroll-horizontal {
  0% {
    transform: translateX(var(--start-distance));
  }
  100% {
    transform: translateX(var(--end-distance));
  }
}
</style>
